跳到主要内容

Java UDP编程

使用 UDP 通信

大部分复制自(写的好棒): UDP编程

和 TCP 编程相比,UDP 编程就简单得多,因为 UDP 没有创建连接,数据包也是一次收发一个,所以没有流的概念。UDP就像发短信:不用确认对方是否在线,只需知道对面地址就可以发短信了

因为它的性质,所以没有严格的服务端和客户端区别(还是有服务端和客户端,但是服务端无需等待到客户端连接上才能执行后面的操作,它是直接监听消息的)

在 Java 中使用 UDP 编程,仍然需要使用 Socket,因为应用程序在使用 UDP 时必须指定网络接口(IP)和端口号。注意:UDP端口和 TCP 端口虽然都使用 0~65535,但他们是两套 独立的端口,即一个应用程序用 TCP 占用了端口1234,不影响另一个应用程序用 UDP 占用端口 1234。

DatagramSocket

服务器端首先使用如下语句在指定的端口监听 UDP 数据包:

DatagramSocket ds = new DatagramSocket(6666);

客户端打开一个 DatagramSocket 使用以下代码:

DatagramSocket ds = new DatagramSocket();
ds.setSoTimeout(1000);
ds.connect(InetAddress.getByName("localhost"), 6666);

客户端创建 DatagramSocket 实例时并不需要指定端口,而是由操作系统自动指定一个当前未使用的端口。紧接着,调用 setSoTimeout(1000) 设定超时1秒,意思是后续接收 UDP 包时,等待时间最多不会超过1秒,否则在没有收到 UDP包时,客户端会无限等待下去。这一点和服务器端不一样,服务器端可以无限等待,因为它本来就被设计成长时间运行。

注意到客户端的 DatagramSocket 还调用了一个 connect() 方法“连接”到指定的服务器端。不是说UDP是无连接的协议吗?为啥这里需要 connect()

这个 connect() 方法不是真连接,它是为了在客户端的 DatagramSocket 实例中保存服务器端的 IP 和端口号,确保这个 DatagramSocket 实例只能往指定的地址和端口发送 UDP 包,不能往其他地址和端口发送。这么做不是 UDP 的限制,而是 Java 内置了安全检查。

所以如果客户端希望向两个不同的服务器发送 UDP 包,那么它必须创建两个 DatagramSocket 实例。

后续的收发数据和服务器端是一致的。通常来说,客户端必须先发 UDP 包,因为客户端不发 UDP 包,服务器端就根本不知道客户端的地址和端口号。

如果客户端认为通信结束,就可以调用 disconnect() 断开连接,disconnect() 也不是真正地断开连接,它只是清除了客户端 DatagramSocket 实例记录的远程服务器地址和端口号,这样,DatagramSocket 实例就可以连接另一个服务器端。

DatagramPacket

要接收一个 UDP 数据包,需要准备一个 byte[] 缓冲区,并通过 DatagramPacket 实现接收:

byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
ds.receive(packet);

假设我们收取到的是一个 String,那么,通过 DatagramPacket 返回的 packet.getOffset()packet.getLength() 确定数据在缓冲区的起止位置:

String s = new String(packet.getData(), packet.getOffset(), packet.getLength(), StandardCharsets.UTF_8);

当服务器收到一个 DatagramPacket 后,通常必须立刻回复一个或多个 UDP 包,因为客户端地址在 DatagramPacket 中,每次收到的 DatagramPacket 可能是不同的客户端,如果不回复,客户端就收不到任何 UDP 包。

发送 UDP 包也是通过 DatagramPacket 实现的,发送代码非常简单:

byte[] data = ...
packet.setData(data);
ds.send(packet);

服务端

DatagramSocket ds = new DatagramSocket(6666); // 监听指定端口
for (;;) { // 无限循环
// 数据缓冲区:
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
ds.receive(packet); // 收取一个UDP数据包
// 收取到的数据存储在buffer中,由packet.getOffset(), packet.getLength()指定起始位置和长度
// 将其按UTF-8编码转换为String:
String s = new String(packet.getData(), packet.getOffset(), packet.getLength(), StandardCharsets.UTF_8);
// 发送数据:
byte[] data = "ACK".getBytes(StandardCharsets.UTF_8);
packet.setData(data);
ds.send(packet);
}

客户端

DatagramSocket ds = new DatagramSocket();
ds.setSoTimeout(1000);
ds.connect(InetAddress.getByName("localhost"), 6666); // 连接指定服务器和端口
// 发送:
byte[] data = "Hello".getBytes();
DatagramPacket packet = new DatagramPacket(data, data.length);
ds.send(packet);
// 接收:
byte[] buffer = new byte[1024];
packet = new DatagramPacket(buffer, buffer.length);
ds.receive(packet);
String resp = new String(packet.getData(), packet.getOffset(), packet.getLength());
ds.disconnect();

基本的通信

使用 UDP 协议通信时,服务器和客户端双方无需建立连接:

  • 服务器端用 DatagramSocket(port) 监听端口;
  • 客户端使用 DatagramSocket.connect() 指定远程地址和端口;
  • 双方通过 receive()send() 读写数据;
  • DatagramSocket 没有 IO流接口,数据被直接写入 byte[] 缓冲区。

发送消息给目标

/**
* 不需要连接服务器
* @author alsritter
* @version 1.0
**/
public class UDPClientDemo {
public static void main(String[] args) throws Exception {
//1. 建立一个Socket
DatagramSocket socket = new DatagramSocket();
//2. 建个包
String msg = "这是一条发送给服务器的消息";
//发送的目标
InetAddress localhost = InetAddress.getByName("localhost");
int port = 9090;
//数据,数据的长度(起始),发送的目标
DatagramPacket packet = new DatagramPacket(msg.getBytes(),0,msg.getBytes().length,localhost,port);

//3. 发送包
socket.send(packet);
// 关闭流
socket.close();
}
}

接收消息

/**
* 还是要等客户端的连接
* @author alsritter
* @version 1.0
**/
public class UDPServerDemo {
public static void main(String[] args) throws Exception {
//开放端口
DatagramSocket socket = new DatagramSocket(9090);
//接收数据包(这个buffer这样写是指最大只能接收1k)
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer,0,buffer.length);
socket.receive(packet); //阻塞接收

//显示包裹的信息(注意这个Port是客户端的端口,不是服务端的)
System.out.println("发送地址:" + packet.getAddress()
+ " 端口:" + packet.getPort()
+ " 数据长度:" + packet.getLength()
+ " 数据:" + new String(packet.getData()));

//关闭连接
socket.close();
}
}

多线程实现聊天室

因为需要同时发消息,收消息,所以需要用到多线程

发送消息的线程

/**
* 发送消息的线程
* @author alsritter
* @version 1.0
**/
public class TalkSend implements Runnable {
private DatagramSocket socket = null;
private BufferedReader reader = null;
// 目标IP地址
private String toIp;
// 当前自己的端口
private int fromPort;
// 目标端口
private int toPort;

public TalkSend(String toIp, int fromPort, int toPort){
this.toIp = toIp;
this.fromPort = fromPort;
this.toPort = toPort;
try {
socket = new DatagramSocket(this.fromPort);
//准备数据:控制台读取 System.in
reader = new BufferedReader(new InputStreamReader(System.in));
} catch (SocketException e) {
e.printStackTrace();
}

}

@Override
public void run() {
while (true){
String data = null;
try {
data = reader.readLine();
byte[] dataset = data.getBytes();
DatagramPacket packet = new DatagramPacket(dataset,0,dataset.length,
new InetSocketAddress(this.toIp,this.toPort));

socket.send(packet);
//如果是 bye 则断开连接
if (data.equals("bye")) {
break;
}
} catch (IOException e) {
e.printStackTrace();
}
}
socket.close();
}
}

接收消息的线程

/**
* 收消息的线程
* @author alsritter
* @version 1.0
**/
public class TalkReceive implements Runnable{

private DatagramSocket socket = null;
// 当前自己的端口(与上面那个自己端口不同)
private int fromPort;
// 是谁发送的消息
private String msgFrom;

public TalkReceive(int fromPort,String msgFrom) {
this.msgFrom = msgFrom;
this.fromPort = fromPort;
try {
socket = new DatagramSocket(this.fromPort);
} catch (SocketException e) {
e.printStackTrace();
}
}

@Override
public void run() {

while (true){
//准备接收包裹
byte[] container = new byte[1024];
DatagramPacket packet = new DatagramPacket(container,0,container.length);
try {
socket.receive(packet);//阻塞式接收包裹
//如果是 bye 则断开连接
byte[] data = packet.getData();
String receiveData = new String(data,0,packet.getLength());
System.out.println(msgFrom+": "+ receiveData);
if (receiveData.equals("bye")) {
break;
}
} catch (IOException e) {
e.printStackTrace();
}

}
socket.close();
}
}

编写测试方法

使用这两个工具类,模拟两个人。所以定义两个类

/**
* 房主
*
* @author alsritter
* @version 1.0
**/
public class RoomHost {
public static void main(String[] args) {
//开启两个线程
new Thread(new TalkSend("localhost",5555,8888)).start();
//这个消息是从群友A那里传来的
new Thread(new TalkReceive(9999,"群友A")).start();
}
}
/**
* 群友
*
* @author alsritter
* @version 1.0
**/
public class User {
public static void main(String[] args) {
//开启两个线程
new Thread(new TalkSend("localhost",7777,9999)).start();
//这个消息是从群主那里传来的
new Thread(new TalkReceive(8888,"群主")).start();
}
}